feat: make MoneyAccountUpgradeController own its bootstrap - #10072
Conversation
|
@metamaskbot publish-preview |
1 similar comment
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
MoneyAccountUpgradeController (core PR MetaMask/core#10072) now owns its bootstrap: it watches the feature-flag and keyring state, parses the vault config, and serializes its own re-inits. The extension supplies only the client-specific parts as hooks — the basic-functionality gate inside isEnabled, the fail-closed geo check as isEligible, the shared Money chain configurator, and Sentry reporting for a missing vault config — and MoneyAccountUpgradeService is deleted. Per review feedback, no messenger calls happen at construction: metamask-controller calls controller.init() once everything is built, alongside the other post-construction init() calls, so the init map no longer carries ordering constraints. Onboarding and preferences changes re-trigger the controller's gating through its public sync(). The package is pinned to a preview build until core releases @metamask/money-account-upgrade-controller@4.0.0 and @metamask/money-account-utils@1.2.0; the resolutions entries and the ^3.0.2 dependency swap to ^4.0.0 / ^1.2.0 then. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1ee21f9 to
4258cb1
Compare
4258cb1 to
81700ed
Compare
|
@metamaskbot publish-preview |
2 similar comments
|
@metamaskbot publish-preview |
|
@metamaskbot publish-preview |
a302de5 to
6ec257f
Compare
|
@metamaskbot publish-preview |
ad38c64 to
ce71f7e
Compare
mcmire
left a comment
There was a problem hiding this comment.
I have one comment below, but everything else seems good to me.
| | ControllerStateChangedEvent<'KeyringController', KeyringControllerState> | ||
| | ControllerStateChangedEvent< | ||
| 'RemoteFeatureFlagController', | ||
| RemoteFeatureFlagControllerState | ||
| >; |
There was a problem hiding this comment.
Types already exist for these events, is it possible to use those?
| | ControllerStateChangedEvent<'KeyringController', KeyringControllerState> | |
| | ControllerStateChangedEvent< | |
| 'RemoteFeatureFlagController', | |
| RemoteFeatureFlagControllerState | |
| >; | |
| type AllowedEvents = | |
| | KeyringControllerStateChangeEvent | |
| | RemoteFeatureFlagControllerStateChangeEvent |
(You have may encountered a lint rule prohibiting use of :stateChange events but I plan on removing that.)
The controller now subscribes to RemoteFeatureFlagController and KeyringController state, parses the moneyAccountVaultConfig flag, and runs its own serialized bootstrap, replacing the client-side services that previously drove init() externally. Client-specific concerns (version-gated enable flag, eligibility checks, adding the vault chain, error reporting) are supplied as constructor hooks. - BREAKING: init() is now the no-argument lifecycle entry point (subscribe and sync); the config-arming routine is internal and driven by the moneyAccountVaultConfig remote feature flag - BREAKING: the constructor requires a hooks option with an isEnabled hook - upgradeAccount() now waits for an in-flight bootstrap instead of throwing, and refuses to run against a stale config after the feature is disabled - Add vault-config parsing and comparison helpers to @metamask/money-account-utils, shared by the controller and both clients Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A disarm (isEnabled flipping off) or a newer scheduled config during the CHOMP service-details call now supersedes the in-flight run instead of the run arming a config the controller had already dropped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scheduling a bootstrap for a changed vault config now disarms the armed config, so upgradeAccount waits for the re-bootstrap (or refuses if it failed) instead of signing delegations against the old vault. The wait also follows runs chained onto the bootstrap while waiting, so a call that captured a superseded run no longer throws while its successor is still running. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Make `underlyingToken` optional in the shared vault-config parser so flags that predate the field still parse, matching the balance service - Re-check the armed config before every upgrade step and abort if it is disarmed or superseded mid-sequence - Refuse `upgradeAccount` while the wallet is locked - Contain a throwing `onBootstrapError` hook so the failed bootstrap is still retried and nothing escapes `init()` or `sync()` Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
5f5d356 to
0046818
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0046818. Configure here.
| if (!vaultConfig) { | ||
| this.#reportMissingConfig(); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Stale vault config stays armed
Medium Severity
When isEnabled is on but moneyAccountVaultConfig is unserved or malformed, sync() reports MissingMoneyAccountVaultConfigError and returns without clearing #config or #bootstrappedConfig. An already-armed controller keeps signing upgrades against the last vault, which contradicts the hook comment that this path disables upgrades and the sibling balance service, which drops the previous vault when the same flag is absent or invalid.
Reviewed by Cursor Bugbot for commit 0046818. Configure here.
MoneyAccountUpgradeController (core PR MetaMask/core#10072) now owns its bootstrap: it watches the feature-flag and keyring state, parses the vault config, and serializes its own re-inits. The extension supplies only the client-specific parts as hooks — the basic-functionality gate inside isEnabled, the fail-closed geo check as isEligible, the shared Money chain configurator, and Sentry reporting for a missing vault config — and MoneyAccountUpgradeService is deleted. Per review feedback, no messenger calls happen at construction: metamask-controller calls controller.init() once everything is built, alongside the other post-construction init() calls, so the init map no longer carries ordering constraints. Onboarding and preferences changes re-trigger the controller's gating through its public sync(). The package is pinned to a preview build until core releases @metamask/money-account-upgrade-controller@4.0.0 and @metamask/money-account-utils@1.2.0; the resolutions entries and the ^3.0.2 dependency swap to ^4.0.0 / ^1.2.0 then. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>


Explanation
This updates the money account upgrade controller to make it better able to handle its own initialisation. At the moment clients have to manage initialising the controller, leading to a lot of shared code. This PR tries to move that logic into the controller itself as much as possible
Key changes:
RemoteFeatureFlagController:stateChangedandKeyringController:stateChangedand uses this information to bootstrap itself.init()now takes no arguments, and merely sets up subscriptions to to some messenger events.hooksoption for the parts of the upgrade flow that can only be determined in the client.upgradeAccount()waits for an in-flight bootstrap instead of throwing (replacing mobile's module-levelwhenMoneyAccountUpgradeReadypromise), and is disarmed whenisEnabledflips off so it cannot run against a stale config.sync()lets clients re-trigger the bootstraps checks from client-only signals - for example onboarding being completed.@metamask/money-account-utils(getMoneyAccountVaultConfig,parseMoneyAccountVaultConfig,areMoneyAccountVaultConfigsEqual,MoneyAccountVaultConfig), shared by the controller and both clients.References
app/core/Engine/controllers/money-account-upgrade-controller-init.ts(follow-up PR required)Checklist
Note
High Risk
Breaking API and messenger wiring for all clients, plus signing/EIP-7702 upgrade flows that depend on serialized bootstrap and config disarm logic; incorrect integration could block upgrades or sign against wrong vault config.
Overview
MoneyAccountUpgradeControllernow bootstraps itself instead of requiring clients to callinit({ chainId, boringVaultAddress }). After a one-time, no-arginit(), it listens to remote feature flags and keyring state, gates on clienthooks(isEnabled, optionalisEligible,ensureChainConfigured,onBootstrapError), parsesmoneyAccountVaultConfig, and arms upgrade config from CHOMP. A publicsync()re-runs those gates for client-only signals (e.g. onboarding).Breaking consumer changes: constructor must pass
hooks; messenger must allowRemoteFeatureFlagController:getState,KeyringController:getState, and the correspondingstateChangeevents.upgradeAccount()waits for in-flight bootstrap, refuses when disarmed (feature off, failed bootstrap, locked wallet), and aborts mid-sequence if the vault config is superseded—so delegations are not signed against a stale vault.Vault flag parsing moves to
@metamask/money-account-utils(getMoneyAccountVaultConfig,parseMoneyAccountVaultConfig,areMoneyAccountVaultConfigsEqual,MissingMoneyAccountVaultConfigErrorsurfaced viaonBootstrapErroronce when the flag is missing/malformed).Reviewed by Cursor Bugbot for commit 0046818. Bugbot is set up for automated code reviews on this repo. Configure here.